Adding topics to 11ty
Introduction
When I was starting my blog I quickly realised that I wanted to by able to group posts by topics, so that user could dive into specific areas of interest - rather than having to browse through everything. I couldn't find anything native in eleventy to do this for me, so I started working out how to do it. This post is a high level overview of how I approached it. I've simplified some areas, but hopefully this gives you some clues. The process of adding topics taught me a lot about how eleventy works and how powerful customisations can be - hopefully it helps you too! You can always find me on Mastadon if you have any questions
Note that I use nunjacks syntax, things will be different if you use Liquid or so on
Adding a topic to a post
We start by adding a topic
property to the front matter of post markdown files. This makes it easy to assign a post to a topci
---
title: foo
topic: leadership
---
Happily, we don't need to do anything else to be able to add properties to front matter. You don't need to register the property in the eleventy config or anything else. We can then access this on the data property of the post object - I tend to run a null check as well (assuming a default value), just in case the post hasn't been assigned a topic yet
const topic = post.data?.topic || 'unclassified';
Adding topic header data
Now that posts have topics, we need to display the topics themselves somehow. To achieve this, let's start by adding some topic data. I added a new js file into the _data
folder called topicdata.js, looking something like this:
module.exports = {
leadership: {
displayOrder: 1,
title: "leadership",
description: "Posts around leadership"
},
coding: {
displayOrder: 2,
title: "coding",
description: "The coding topic"
}
}
Just adding the file makes the data available throughout the rest of the site, with no additional configuration. For example, we access this in layout files like so:
{% set topics = topicdata | orderedTopicKeys %}
You'll notice here that we have referenced the data purely by name - we don't need to do anything else. You may also have noticed | orderedTopicKeys
. That's a filter, we'll come back to that when we discuss filters in a moment
Listing topics
Now we can assign topics to posts, it would be nice to be able to let the user see them. We start by adding a file to list the topics, mine is called _includes/topiclist.njk
A simplified version looks like this:
<div class="topic-container">
{% set topics = topicdata | orderedTopicKeys %}
{% for topicKey in topics %}
{% set topicValue = topicdata[topicKey] %}
<div class="topic">
<h2><a href="./topics/{{ topicKey }}/">{{ topicValue.title }}</a></h2>
<section class="topic-description">
{{ topicValue.description }}
</section>
</div>
{% endfor %}
</div>
Let's break down what's going on:
- Grab a list of topic keys in display order and a map of posts, grouped by topic (note the use of the
| orderTopicKeys
filter, we're coming to that very soon - I promise)
{% set topics = topicdata | orderedTopicKeys %}
- loop around the topic keys
{% for topicKey in topics %}
- grab each topic in turn (this will be the object from the
topicdata
json file)
{% set topicValue = topicdata[topicKey] %}
- display information about the topic - note that properties such as
title
anddescription
are simply properties of the topic object from thetopicdata
file
<div class="topic">
<h2><a href="./topics/{{ topicKey }}/">{{ topicValue.title }}</a></h2>
<section class="topic-description">
{{ topicValue.description }}
</section>
</div>
Filtering topic data
I keep promising to dicuss the orderTopicKeys
filter, so let's do it now. Filters are a powerful feature of eleventy and allow you to transform the data that you pass to them - even better, you can chain them together. To add filters we need to add a file to contain the filter. Mine is called eleventy.config.topics
and looks something like this:
module.exports = eleventyConfig => {
eleventyConfig.addFilter("orderedTopicKeys", function orderedTopicKeys(items) {
const orderedTopics = [];
// create a map of topic key and display order
for (const key in items) {
orderedTopics.push({ topic: key, displayOrder: items[key]?.displayOrder })
};
// order this by display order
const topics = orderedTopics.sort((a, b) => {
if (a.displayOrder < b.displayOrder) return -1;
if (a.displayOrder > b.displayOrder) return 1;
return 0;
});
// return the ordered topics
return topics.map(t => t.topic);
})
}
Then we need to register the filter in our eleventy configuration. Happily this is straightforward, you just update your eleventy.config.js
as follows (note that you will almost certainly have a lot more config than this)
const pluginTopics = require("./eleventy.config.topics.js");
module.exports = function(eleventyConfig) {
/* omitted for clarity */
eleventyConfig.addPlugin(pluginTopics);
/* omitted for clarity */
}
I said I'd explain how this all works, so let's get into that. There's quite a bit going on here, so let's break down how it hangs together:
-
This statement in the layout file `````` grabs
topicdata
(which is the global data from earlier) and feeds it intoorderedTopicKeys
-
The
orderedTopicKeys
filter receivestopicdata
in the input parameteritems
(nothing fancy about names here - it's just the first parameter):
eleventyConfig.addFilter("orderedTopicKeys", function orderedTopicKeys(items) {
orderedTopicKeys
then applies a sort to thetopicdata
initems
- note that displayOrder is just a JSON property of the topic intopcidata
const topics = orderedTopics.sort((a, b) => {
if (a.displayOrder < b.displayOrder) return -1;
if (a.displayOrder > b.displayOrder) return 1;
return 0;
});
And that's it - fairly straight forward.
If you wanted to chain filters, you do it like this:
{% set foo = somedata | filter1 | filter2 %}
Where the output of filter1 is passed to filter2, and so on
Listing posts within a topic
Did you notice how I didn't mention anything about the link to the topics page, then I added the topiclist.njk
template? This line:
<h2><a href="./topics//"></a></h2>
Obviously we need to generate a page per the topic to support this. We could just add these manually, of course, but wouldn't it be nicer if we could do it automatically based on our topicdata
json? Well guess what, we can. I created a file called topic.njk
in the content
folder for this purpose. It looks something like this:
---
pagination:
data: topicdata
size: 1
alias: topic
addAllPagesToCollections: true
layout: layouts/base.njk
eleventyComputed:
title: Topic “{{ topic }}”
permalink: /topics/{{ topic | slugify }}/
---
{% set topicData = topicdata[topic] %}
<h2>{{ topicData.title }}</h2>
<p>{{ topicData.description }}</p>
{% set postslist = collections.posts | filterPostsByTopic(topic) %}
{% include "postslist.njk" %}
As usual, let's examine what's happening here, starting with the front matter. The key to really understanding this is to remember that eleventy is a static site generator; think of it like a compiler for your website that is going to 'compile' a page for each property of the topicdata
- Here we tell eleventy to compile a version of this page for each property of
topicdata
- aliasing the current item as a variable calledtopic
pagination:
data: topicdata
size: 1
alias: topic
addAllPagesToCollections: true
- Next, we automatically assign a title, based on the topic
eleventyComputed:
title: Topic “coding”
- And finally we create a permalink to this page (this is what we referenced earlier in
topiclist.njk
):
permalink: /topics/coding/
Having sorted out the front matter, let's look at the page content itself
- We start by fetching the actual topic object from
topicdata
(thetopic
variable is simply the property name) and then display some basic topic data
Example value of topicData
variable, taken from the from topicdata
global data file (see the top of this post if you've forgotten this):
{
displayOrder: 2,
title: "coding",
description: "Posts relating to coding - this covers all sorts of things relating to the land of curly brackets",
}
Example of how we acess and use this in the template:
{% set topicData = topicdata[topic] %}
<h2>{{ topicData.title }}</h2>
<p>{{ topicData.description }}</p>
- Finally we grab the posts that reference this topic, and display them
{% set postslist = collections.posts | filterPostsByTopic(topic) %}
{% include "postslist.njk" %}
Adding the posts
OK, so I may have glossed over the bit where we add the posts into the topic data file just now. There aren't any new concepts here, but let's cover it anyway:
We just need to add a template called 'postslist.njk', as follows:
<ul class="postlist">
{% for post in postslist | reverse %}
<li class="postlist-item">
<a href="{{ post.url }}" class="postlist-link">{% if post.data.title %}{{ post.data.title }}{% else %}{{ post.url }}{% endif %}</a></h2>
<time class="postlist-date" datetime="{{ post.date | htmlDateString }}">{{ post.date | readableDate }}</time>
<p>{{ post.data.description }}</p>
</li>
{% endfor %}
</ul>
Notice that the statement {% for post in postslist | reverse %}
reference the postslist
variable that we set in the topiclist.njk
template. Let's quickly look at how that bit works:
Template: topiclist.njk
<!-- set the post list variable -->
{% set postslist = collections.posts | filterPostsByTopic(topic) %}
<!-- call the template -->
{% include "postslist.njk" %}
File: postslist.njk
<!-- we can use this variable in the template now -->
{% for post in postslist | reverse %}
You can also see a sneaky
| reverse
filter that is applied to the variable, to reverse the order of the posts
Everything after that is just us chosing the bits of the post we want to display
Adding topics to my homepage
The final piece of this is to show the list of topics in the index page - as I want that to be the default view for my site. We can do this by just reference the topiclist.njk
in the index.njk, like this:
---
layout: layouts/home.njk
eleventyNavigation:
key: topics
order: 10
---
{% include "topiclist.njk" %}
References
I've skimmed over a lot of deeper topics here, I'd recommend having a read of the following if you want more details:
What next?
I don't have a comment section on my blog at the moment, but I'm always happy to chat on Mastadon- Previous post: Why do people work for you?
- Next post: Structuring Docker files to improve build times